#!/usr/bin/env bash
# Install or remove the packaged MAKO Renderer without requiring a terminal.
set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
install_prefix="${MAKO_INSTALL_PREFIX:-$HOME/.local}"
state_dir="$install_prefix/share/mako-render/installer"
state_manifest="$state_dir/installed-files.sha256"
active_renderer_state="$install_prefix/share/mako-render/active-renderer.json"
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/mako-render"
payload_manifest="$script_dir/MAKO-Renderer-install-manifest.txt"
version_file="$script_dir/MAKO-Renderer-version.txt"
decky_plugin_manifest="$HOME/homebrew/plugins/Mako/plugin.json"
decky_install_prefix="$HOME/.local"
configuration_desktop_path="share/applications/io.github.eugeniosegala.mako.desktop"
uninstaller_desktop_path="share/applications/io.github.eugeniosegala.mako.uninstaller.desktop"
dialog_title="MAKO Renderer"
dialog_icon_name="io.github.eugeniosegala.mako"
dialog_icon_path=""
dialog_data_dirs="$script_dir/share:$script_dir/../share:$install_prefix/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
for candidate_icon in \
        "$script_dir/share/icons/hicolor/256x256/apps/$dialog_icon_name.png" \
        "$script_dir/../share/icons/hicolor/256x256/apps/$dialog_icon_name.png" \
        "$install_prefix/share/icons/hicolor/256x256/apps/$dialog_icon_name.png"; do
    if [[ -f "$candidate_icon" ]]; then
        dialog_icon_path="$candidate_icon"
        break
    fi
done
decky_renderer_relative_paths=(
    ".local/share/mako-render/lib/libmako-render.so"
    ".local/share/mako-render/lib32/libmako-render.so"
    ".local/share/mako-render/lib/libmako-render-scaling.so"
    ".local/share/mako-render/lib32/libmako-render-scaling.so"
    ".local/share/mako-render/vulkan/implicit_layer.d/VkLayer_MAKO_render.json"
    ".local/share/mako-render/vulkan/implicit_layer.d/VkLayer_MAKO_render.x86.json"
    ".local/share/mako-render/vulkan/spatial_scaling.d/VkLayer_MAKO_spatial_scaling.json"
    ".local/share/mako-render/vulkan/spatial_scaling.d/VkLayer_MAKO_spatial_scaling.x86.json"
    ".local/share/mako-render/vulkan/gamescope_wsi_compatibility.d/VkLayer_FROG_gamescope_wsi.x86_64.json"
    ".local/share/mako-render/vulkan/gamescope_wsi_compatibility.d/libVkLayer_FROG_gamescope_wsi_x86_64.so"
    ".local/share/mako-render/vulkan/mangohud.d/MangoHud.x86_64.json"
    ".local/share/mako-render/vulkan/mangohud.d/MangoHud.x86.json"
    ".local/share/mako-render/vulkan/vkbasalt.d/vkBasalt.json"
    ".local/share/mako-render/vulkan/vkbasalt.d/vkBasalt.x86.json"
    ".local/share/mako-render/bin/mako-cli"
    ".local/share/mako-render/installed-engine.json"
    ".local/share/mako-render/active-renderer.json"
    ".local/bin/mako-run"
    ".local/bin/mako-diagnostics"
    ".local/share/vulkan/implicit_layer.d/VkLayer_MAKO_render.json"
    ".local/share/vulkan/implicit_layer.d/VkLayer_MAKO_render.x86.json"
)

kdialog_mako() {
    env XDG_DATA_DIRS="$dialog_data_dirs" \
        kdialog --title "$dialog_title" --icon "$dialog_icon_name" "$@"
}

zenity_mako() {
    local -a options=(--title="$dialog_title")
    if [[ -n "$dialog_icon_path" ]]; then
        options+=(--window-icon="$dialog_icon_path")
    fi
    zenity "${options[@]}" "$@"
}

fail() {
    local message="$1"
    if [[ "${MAKO_INSTALLER_ASSUME_YES:-0}" != "1" ]] &&
            { [[ -n "${DISPLAY:-}" ]] || [[ -n "${WAYLAND_DISPLAY:-}" ]]; }; then
        if command -v kdialog >/dev/null 2>&1; then
            kdialog_mako --error "$message" || true
        elif command -v zenity >/dev/null 2>&1; then
            zenity_mako --error --text="$message" || true
        fi
    fi
    printf 'MAKO installer: %s\n' "$message" >&2
    exit 1
}

notice() {
    local message="$1"
    if [[ "${MAKO_INSTALLER_ASSUME_YES:-0}" != "1" ]] &&
            { [[ -n "${DISPLAY:-}" ]] || [[ -n "${WAYLAND_DISPLAY:-}" ]]; }; then
        if command -v kdialog >/dev/null 2>&1; then
            kdialog_mako --msgbox "$message" || true
            return
        fi
        if command -v zenity >/dev/null 2>&1; then
            zenity_mako --info --text="$message" || true
            return
        fi
    fi
    printf '%s\n' "$message"
}

confirm() {
    local message="$1"
    if [[ "${MAKO_INSTALLER_ASSUME_YES:-0}" == "1" ]]; then
        return 0
    fi
    if [[ -n "${DISPLAY:-}" || -n "${WAYLAND_DISPLAY:-}" ]]; then
        if command -v kdialog >/dev/null 2>&1; then
            kdialog_mako --yesno "$message"
            return
        fi
        if command -v zenity >/dev/null 2>&1; then
            zenity_mako --question --text="$message"
            return
        fi
    fi
    local answer
    printf '%s [y/N] ' "$message" >&2
    read -r answer
    [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]
}

validate_relative_path() {
    local path="$1"
    case "$path" in
        ""|/*|.|..|../*|*/../*|*/..)
            return 1
            ;;
    esac
    return 0
}

parse_manifest_line() {
    local line="$1"
    manifest_hash="${line%% *}"
    manifest_path="${line#"$manifest_hash"}"
    manifest_path="${manifest_path#  }"
    [[ "$manifest_hash" =~ ^[a-fA-F0-9]{64}$ ]] &&
        validate_relative_path "$manifest_path" ||
        fail "The MAKO installation manifest is invalid."
}

validate_manifest() {
    local manifest="$1"
    local line
    local entries=0
    [[ -f "$manifest" ]] || fail "Missing MAKO installation manifest: $manifest"
    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        entries=$((entries + 1))
    done < "$manifest"
    ((entries > 0)) || fail "The MAKO installation manifest is empty."
}

manifest_contains_path() {
    local manifest="$1"
    local wanted_path="$2"
    local line
    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        [[ "$manifest_path" == "$wanted_path" ]] && return 0
    done < "$manifest"
    return 1
}

hash_file() {
    sha256sum -- "$1" | awk '{print $1}'
}

quote_desktop_exec_path() {
    local value="$1"
    [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] ||
        fail "The MAKO installation path cannot contain a line break."
    value="${value//\\/\\\\}"
    value="${value//\"/\\\"}"
    value="${value//\`/\\\`}"
    value="${value//\$/\\\$}"
    value="${value//%/%%}"
    printf '"%s"' "$value"
}

rewrite_desktop_exec() {
    local relative_path="$1"
    local executable="$2"
    local arguments="$3"
    local desktop_path="${4:-$install_prefix}/$relative_path"
    local temporary_path="${desktop_path}.mako-install.$$"
    local quoted_executable line
    local exec_lines=0

    manifest_contains_path "$payload_manifest" "$relative_path" || return 0
    [[ -f "$desktop_path" ]] || fail "The installed payload is missing $relative_path."
    quoted_executable="$(quote_desktop_exec_path "$executable")"
    : > "$temporary_path"
    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$line" == Exec=* ]]; then
            printf 'Exec=%s %s\n' "$quoted_executable" "$arguments" >> "$temporary_path"
            exec_lines=$((exec_lines + 1))
        else
            printf '%s\n' "$line" >> "$temporary_path"
        fi
    done < "$desktop_path"
    if ((exec_lines != 1)); then
        rm -f -- "$temporary_path"
        fail "The packaged desktop entry $relative_path must contain exactly one Exec line."
    fi
    chmod --reference="$desktop_path" "$temporary_path"
    mv -f -- "$temporary_path" "$desktop_path"
}

write_installed_state_manifest() {
    local payload_root="$1"
    local output_manifest="$2"
    local temporary_path="$output_manifest.mako-install.$$"
    local line installed_path installed_hash
    : > "$temporary_path"
    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        installed_path="$payload_root/$manifest_path"
        [[ -f "$installed_path" ]] || fail "The installed payload is missing $manifest_path."
        installed_hash="$(hash_file "$installed_path")"
        printf '%s  %s\n' "$installed_hash" "$manifest_path" >> "$temporary_path"
    done < "$payload_manifest"
    mv -fT -- "$temporary_path" "$output_manifest"
}

write_active_renderer_state() {
    local version="$1"
    local output_state="$2"
    local temporary_path="$output_state.mako-install.$$"
    printf '{\n  "schema_version": 1,\n  "owner": "standalone",\n  "version": "%s"\n}\n' \
        "$version" > "$temporary_path"
    chmod 0644 "$temporary_path"
    mv -fT -- "$temporary_path" "$output_state"
}

remove_if_managed() {
    local path="$1"
    local expected_hash="$2"
    [[ -f "$path" ]] || return 0
    if [[ "$(hash_file "$path")" == "$expected_hash" ]]; then
        rm -f -- "$path"
    else
        printf 'MAKO installer: preserving modified file: %s\n' "$path" >&2
    fi
}

remove_empty_parent_directories() {
    local path="$1"
    local root="$2"
    local directory relative_path top_level boundary
    directory="$(dirname -- "$path")"
    relative_path="${directory#"$root"/}"
    [[ "$relative_path" != "$directory" && "$relative_path" == */* ]] ||
        return 0
    top_level="${relative_path%%/*}"
    boundary="$root/$top_level"
    while [[ "$directory" != "$boundary" ]]; do
        rmdir -- "$directory" 2>/dev/null || break
        directory="$(dirname -- "$directory")"
    done
}

warn_decky_installation() {
    [[ -f "$decky_plugin_manifest" ]] || return 0
    local message="Warning: MAKO Decky is installed at ${decky_plugin_manifest%/plugin.json}. Continuing selects this standalone Renderer version for both launch workflows. If it differs from Decky's bundled version, MAKO Decky will offer its normal Renderer update. Continue with the standalone installation?"
    printf 'MAKO installer: %s\n' "$message" >&2
    confirm "$message"
}

# These backups exist only during an explicit install, never in a game path.
install_staging=
install_replacing=0
install_committed=0
install_destinations=()
install_backups=()

finish_install() {
    local status=$? index directory destination restore_failed=0
    trap - EXIT HUP INT TERM
    if [[ "$install_replacing" == 1 && "$install_committed" != 1 ]]; then
        for ((index=${#install_destinations[@]}-1; index>=0; index--)); do
            destination="${install_destinations[index]}"
            directory="${install_backups[index]}"
            if [[ -e "$directory/previous" || -L "$directory/previous" ]]; then
                if ! mv -fT -- "$directory/previous" "$destination"; then
                    printf 'MAKO Renderer: recovery backup retained at %s\n' "$directory" >&2
                    install_backups[index]=
                    restore_failed=1
                fi
            elif ! rm -f -- "$destination"; then
                restore_failed=1
            fi
        done
    fi
    for directory in "${install_backups[@]}" "$install_staging"; do
        [[ -n "$directory" ]] || continue
        rm -rf -- "$directory" || printf 'MAKO Renderer: could not remove staging directory %s\n' "$directory" >&2
    done
    if ((status != 0)); then
        if ((restore_failed)); then
            fail "The Renderer update failed and some files could not be restored. Recovery backups were retained beside the affected files. Check destination permissions and free space before retrying."
        fi
        fail "The Renderer update failed. The previous installation was preserved. Check the package, destination permissions, and free space before retrying."
    fi
}

prepare_install_destination() {
    local destination="$1" source="${2:-}" directory
    if [[ -e "$destination" && ! -f "$destination" && ! -L "$destination" ]]; then
        fail "The installation destination is not a file: $destination"
    fi
    mkdir -p -- "$(dirname -- "$destination")"
    directory="$(mktemp -d "$(dirname -- "$destination")/.mako-install-XXXXXX")"
    install_backups+=("$directory")
    install_destinations+=("$destination")
    if [[ -e "$destination" || -L "$destination" ]]; then
        cp -Pp -- "$destination" "$directory/previous"
    fi
    if [[ -n "$source" ]]; then
        cp -p -- "$source" "$directory/replacement"
    fi
}

install_payload() {
    [[ -f "$payload_manifest" && -f "$version_file" ]] ||
        fail "Open ‘Install MAKO Renderer’ from the extracted MAKO Renderer archive."

    if ! warn_decky_installation; then
        exit 0
    fi

    local version
    version="$(tr -d '[:space:]' < "$version_file")"
    [[ -n "$version" ]] || fail "The MAKO Renderer version file is empty."
    [[ "$version" =~ ^[0-9A-Za-z][0-9A-Za-z._+-]*$ ]] ||
        fail "The MAKO Renderer version file is invalid."

    validate_manifest "$payload_manifest"
    if [[ -f "$state_manifest" ]]; then
        validate_manifest "$state_manifest"
    fi

    if ! confirm "Install MAKO Renderer $version to $install_prefix? Existing MAKO Renderer files in that location will be safely updated. Your profiles will be kept."; then
        exit 0
    fi

    trap finish_install EXIT
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 143' TERM
    mkdir -p -- "$install_prefix"
    install_staging="$(mktemp -d "$install_prefix/.mako-install-XXXXXX")"

    local source_path staged_path line old_path old_hash index
    # Validate and rewrite every payload before touching the selected Renderer.
    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        source_path="$script_dir/$manifest_path"
        [[ -f "$source_path" ]] || fail "The package is missing $manifest_path."
        staged_path="$install_staging/payload/$manifest_path"
        mkdir -p -- "$(dirname -- "$staged_path")"
        cp -p -- "$source_path" "$staged_path"
        [[ "$(hash_file "$staged_path")" == "$manifest_hash" ]] ||
            fail "The package file $manifest_path does not match its recorded checksum."
    done < "$payload_manifest"

    rewrite_desktop_exec "$configuration_desktop_path" "$install_prefix/bin/mako-ui" "%U" "$install_staging/payload"
    rewrite_desktop_exec "$uninstaller_desktop_path" "$install_prefix/bin/mako-installer" "--uninstall" "$install_staging/payload"
    write_installed_state_manifest "$install_staging/payload" "$install_staging/installed-files.sha256"
    write_active_renderer_state "$version" "$install_staging/active-renderer.json"

    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        prepare_install_destination "$install_prefix/$manifest_path" "$install_staging/payload/$manifest_path"
    done < "$payload_manifest"
    if [[ -f "$state_manifest" ]]; then
        while IFS= read -r line || [[ -n "$line" ]]; do
            parse_manifest_line "$line"
            old_path="$manifest_path"
            old_hash="$manifest_hash"
            if ! manifest_contains_path "$payload_manifest" "$old_path"; then
                if [[ -f "$install_prefix/$old_path" && "$(hash_file "$install_prefix/$old_path")" == "$old_hash" ]]; then
                    prepare_install_destination "$install_prefix/$old_path"
                fi
            fi
        done < "$state_manifest"
    fi
    prepare_install_destination "$state_manifest" "$install_staging/installed-files.sha256"
    prepare_install_destination "$active_renderer_state" "$install_staging/active-renderer.json"

    install_replacing=1
    for ((index=0; index<${#install_destinations[@]}; index++)); do
        if [[ -f "${install_backups[index]}/replacement" ]]; then
            mv -fT -- "${install_backups[index]}/replacement" "${install_destinations[index]}"
        else
            rm -f -- "${install_destinations[index]}"
        fi
    done
    install_committed=1
    finish_install

    notice "MAKO Renderer $version is installed. MAKO Renderer Configuration will open now.

For a native Steam or Proton game, use this Steam launch option:
\"$install_prefix/bin/mako-launch\" %command%"
    if [[ "${MAKO_INSTALLER_NO_LAUNCH:-0}" != "1" ]]; then
        if [[ -x "$install_prefix/bin/mako-ui" ]]; then
            (
                "$install_prefix/bin/mako-ui" ||
                    fail "MAKO Renderer is installed, but its configuration app exited with an error. Run \"$install_prefix/bin/mako-ui\" from a terminal to see the error and check configuration permissions or missing Qt dependencies."
            ) &
        else
            notice "MAKO Renderer was installed, but mako-ui is missing. Re-extract the archive and run the installer again."
        fi
    fi
}

uninstall_payload() {
    local purge_configuration="${1:-0}"
    [[ -f "$state_manifest" ]] ||
        fail "No MAKO Renderer installation managed by this installer was found at $install_prefix."

    validate_manifest "$state_manifest"
    local uninstall_message="Remove the managed native MAKO Renderer? Modified files and your profiles will be kept."
    if [[ "$install_prefix" == "$decky_install_prefix" && -f "$decky_plugin_manifest" ]]; then
        uninstall_message="Remove the managed native MAKO Renderer? MAKO Decky will remain installed, but its shared native Renderer will be removed. Open MAKO Decky and select Install Renderer before using MAKO again. Modified files and your profiles will be kept."
    fi
    if ! confirm "$uninstall_message"; then
        exit 0
    fi

    local line
    while IFS= read -r line || [[ -n "$line" ]]; do
        parse_manifest_line "$line"
        remove_if_managed "$install_prefix/$manifest_path" "$manifest_hash"
        remove_empty_parent_directories \
            "$install_prefix/$manifest_path" "$install_prefix"
    done < "$state_manifest"

    if [[ "$install_prefix" == "$decky_install_prefix" ]]; then
        local relative_path
        for relative_path in "${decky_renderer_relative_paths[@]}"; do
            rm -f -- "$HOME/$relative_path"
            remove_empty_parent_directories \
                "$HOME/$relative_path" "$decky_install_prefix"
        done
    else
        rm -f -- "$active_renderer_state"
        remove_empty_parent_directories \
            "$active_renderer_state" "$install_prefix"
    fi
    rm -f -- "$state_manifest"
    remove_empty_parent_directories "$state_manifest" "$install_prefix"
    rmdir -- "$install_prefix/bin" 2>/dev/null || true

    if [[ "$purge_configuration" == "1" ]] ||
            { [[ "${MAKO_INSTALLER_ASSUME_YES:-0}" != "1" ]] &&
                confirm "Also remove your MAKO Renderer profiles, settings, and diagnostics from $config_dir? This cannot be undone."; }; then
        rm -rf -- "$config_dir"
        notice "MAKO Renderer, its configuration, and its diagnostics have been removed."
    else
        notice "MAKO Renderer has been removed. Your profiles remain in $config_dir."
    fi
}

usage() {
    cat <<'EOF'
Usage: mako-installer [--install] [--uninstall [--purge-configuration]]

Run ‘Install MAKO Renderer’ from an extracted MAKO Renderer archive to install
or update the standalone user-local Renderer. Run mako-installer --uninstall
from a default installation to remove the managed native Renderer shared with
MAKO Decky. A custom installation prefix removes only that standalone payload.
EOF
}

mode="install"
purge_configuration=0
while (($#)); do
    case "$1" in
        --install)
            mode="install"
            ;;
        --uninstall)
            mode="uninstall"
            ;;
        --purge-configuration)
            purge_configuration=1
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            usage >&2
            exit 2
            ;;
    esac
    shift
done

case "$mode" in
    install) install_payload ;;
    uninstall) uninstall_payload "$purge_configuration" ;;
esac
